/*
 * XQuery.java
 */

package xq.api;

import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import xq.util.resource.ResourceNotFoundException;
import xq.util.IntRange;
import xq.util.ResourceUtil;
import xq.util.resource.TextResource;
import xq.api.XQVariable.Multiplicity;

/**
 *
 * @author ThorntonRP
 */
public class XQuery implements Serializable {

	private static final long serialVersionUID = 8805865110820424133L;
	
	/**
	 * The default patten for parsing the names and types of resources to be
	 * included in the XQuery statement.
	 */
	private static final Pattern DEFAULT_INCLUDE_PATTERN = Pattern.compile(
			"\\(::\\s*include\\s+(namespaces|variables|functions)\\s+at\\s+([\"'])(.*?)\\2\\s*::\\)");

	/**
	 * The default patten for parsing the names and types of external variables
	 * to be passed to the XQuery statement.
	 */
	private static final Pattern DEFAULT_VARIABLE_PATTERN = Pattern.compile(
			"(?:declare|define)\\s+variable\\s+\\$([^\\s]+)\\s+(?:as\\s+(.*?)([?+*])?\\s+)?external");

	private final Set<XQueryResource> resources;
	private final Map<String, XQVariable> variables;
	private final Set<XQVariable> variableSet;
	
	private String dialect;
	private String queryText;
	private List<String> lines;

	/*--------------------------------------------------------------------
	 * Initialization
	 *--------------------------------------------------------------------*/
	
	/**
	 * Default constructor that initializes internally maintained collections.
	 * This constructor is invoked by all other constructors.
	 */
	private XQuery() {
		this.resources = new LinkedHashSet<XQueryResource>();
		this.variables = new HashMap<String, XQVariable>();
		this.variableSet = new LinkedHashSet<XQVariable>();
	}

	/**
	 * Creates a new XQuery from the given source string, loading any included
	 * resources using the current thread's context classloader.
	 * @param querySource
	 * @throws xq.api.XQException
	 */
	public XQuery(String querySource) throws XQException {
		this(querySource, Thread.currentThread().getContextClassLoader());
	}

	/**
	 * Creates a new XQuery from the given source string, loading any included
	 * resources using the supplied classloader.
	 * @param querySource
	 * @param resourceLoader
	 * @throws xq.api.XQException
	 */
	public XQuery(String querySource, ClassLoader resourceLoader) throws XQException {
		this();
		StringBuilder sb = new StringBuilder();
		processQuerySource(querySource, XQueryResource.Type.MAIN, sb, 0, resourceLoader);
		this.queryText = sb.toString();
	}

	/**
	 * Creates a new XQuery from the given resource, loading any included
	 * resources using the current thread's context classloader.
	 * @param resource
	 * @throws xq.api.XQException
	 */
	public XQuery(TextResource resource) throws XQException {
		this(resource, Thread.currentThread().getContextClassLoader());
	}

	/**
	 * Creates a new XQuery from the given resource, loading any included
	 * resources using the supplied classloader.
	 * @param resource
	 * @param resourceLoader
	 * @throws xq.api.XQException
	 */
	public XQuery(TextResource resource, ClassLoader resourceLoader) throws XQException {
		this();
		StringBuilder sb = new StringBuilder();
		processResource(null, resource, XQueryResource.Type.MAIN, sb, 0, resourceLoader);
		this.queryText = sb.toString();
	}

	/*--------------------------------------------------------------------
	 * Public
	 *--------------------------------------------------------------------*/
	
	/**
	 * Returns the XQuery language dialect as read from the XQuery version
	 * statement. If the query contains no such statement, this property will be
	 * null, and the XQuery service's default dialect will be assumed.
	 * 
	 * @return The XQuery language dialect.
	 */
	public String getDialect() {
		return dialect;
	}
	
	/**
	 * Returns the complete XQuery.
	 * 
	 * @return The complete XQuery.
	 */
	public String getQueryText() {
		return queryText;
	}
	
	/**
	 * Returns the set of external variables declared by the XQuery. If no
	 * variables have been declared, the returned set will be empty.
	 * 
	 * @return External variables declared by the XQuery.
	 */
	public Set<XQVariable> getExternalVariables() {
		return Collections.unmodifiableSet(variableSet);
	}
	
	/**
	 * Returns the external variable having the given name.
	 * 
	 * @param name The fully qualified variable name.
	 * @return The variable having the given name, or null if no such variable
	 *         has been declared.
	 */
	public XQVariable getVariable(String name) {
		return variables.get(name);
	}

	/**
	 * Returns the resources included by this XQuery.
	 * 
	 * @return The set of included resources.
	 */
	public Set<XQueryResource> getResources() {
		return Collections.unmodifiableSet(resources);
	}

	/**
	 * Returns all lines in the XQuery text. This method is useful for
	 * obtaining the source code at a specified line number for diagnostic
	 * purposes.
	 * 
	 * @return Lines of XQuery text.
	 */
	public List<String> getLines() {
		if (lines == null) {
			lines = Arrays.asList(queryText.split("(\r)?\n"));
		}
		return lines;
	}
	
	/**
	 * Returns the XQuery resource that contains the given line. If there is no
	 * such line number, this operation will return null.
	 * @param lineNumber The line number for which to obtain the XQuery resource.
	 * @return The XQuery resource that contains the given line.
	 */
	public XQueryResource getResourceAtLine(int lineNumber) {
		XQueryResource result = null;
		for (XQueryResource resource : resources) {
			result = resource.getResourceAtLine(lineNumber);
			if (result != null) {
				return result;
			}
		}
		return result;
	}

	/**
	 * Returns a string describing the XQuery resource at the given line number.
	 * The returned string will be in the format:
	 * <pre>
	 *     [ResourceFilePath]:[ResourceLineNumber]
	 * </pre>
	 * @param lineNumber
	 * @return
	 */
	public String getLocationString(int lineNumber) {
		if (lineNumber >= getLines().size()) {
			throw new IndexOutOfBoundsException(String.valueOf(lineNumber));
		}
		XQueryResource resource = getResourceAtLine(lineNumber);
		if (resource == null) {
			return null;
		} else {
			int actualLineNumber = lineNumber;
			for (XQueryResource childResource : resource.getChildResources()) {
				if (childResource.getLineOffsets().getEnd() < lineNumber) {
					actualLineNumber -= (childResource.getLineOffsets().getWidth());
				}
			}
			IntRange lineOffsets = resource.getLineOffsets();
			return resource.getResourceURL() + ":" + (actualLineNumber - lineOffsets.getStart() + 1);
		}
	}

	/*--------------------------------------------------------------------
	 * Protected
	 *--------------------------------------------------------------------*/
	
	/**
	 * Provided to allow subclasses to override the syntax for included
	 * XQuery resources.
	 * @return The pattern to use for parsing included resource names.
	 */
	protected Pattern getIncludePattern() {
		return DEFAULT_INCLUDE_PATTERN;
	}

	/**
	 * Provided to allow subclasses to override the syntax for external
	 * variables.
	 * @return The pattern to use for parsing declared external variables.
	 */
	protected Pattern getVariablePattern() {
		return DEFAULT_VARIABLE_PATTERN;
	}

	/**
	 * The regular expression group index for obtaining the included XQuery
	 * resource type.
	 * @return The regular expression group index for the included XQuery
	 *         resource type.
	 */
	protected int getIncludeTypeMatcherGroup() {
		return 1;
	}

	/**
	 * The regular expression group index for obtaining the included XQuery
	 * resource path.
	 * @return The regular expression group index for the included XQuery
	 *         resource path.
	 */
	protected int getIncludePathMatcherGroup() {
		return 3;
	}
	
	/**
	 * The regular expression group index for obtaining the fully qualified
	 * external variable name.
	 * @return The regular expression group index for the fully qualified
	 *         external variable name.
	 */
	protected int getVariableNameMatcherGroup() {
		return 1;
	}

	/**
	 * The regular expression group index for obtaining the external variable
	 * type.
	 * @return The regular expression group index for the external variable
	 *         type.
	 */
	protected int getVariableTypeMatcherGroup() {
		return 2;
	}

	/**
	 * The regular expression group index for obtaining the external variable
	 * multiplicity.
	 * @return The regular expression group index for the external variable
	 *         multiplicity.
	 */
	protected int getVariableMultiplicityMatcherGroup() {
		return 3;
	}

	/*--------------------------------------------------------------------
	 * Private
	 *--------------------------------------------------------------------*/
	
	private int processResource(XQueryResource parentResource, TextResource resource, XQueryResource.Type resourceType, StringBuilder sb, int resourceOffset, ClassLoader resourceLoader) throws XQException {
		if (!resources.contains(resource)) {
			int startOffset = resourceOffset;
			XQueryResource xqueryResource = new XQueryResource(this, parentResource, resource, resourceType);
			int endOffset = processQuerySource(xqueryResource, sb, resourceOffset, resourceLoader);
			IntRange charOffsets = new IntRange(startOffset, true, endOffset, true);
			xqueryResource.setCharOffsets(charOffsets);
			resources.add(xqueryResource);
			resourceOffset = endOffset;
		}
		return resourceOffset;
	}

	private int processQuerySource(XQueryResource resource, StringBuilder sb, int resourceOffset, ClassLoader resourceLoader) throws XQException {
		try {
			if (resource.getType() == XQueryResource.Type.MAIN) {
				parseVariables(resource.getContent());
			}
			Matcher m = getIncludePattern().matcher(resource.getContent());
			int offset = 0;
			while (m.find()) {
				XQueryResource.Type includeType = XQueryResource.Type.valueOf(m.group(getIncludeTypeMatcherGroup()).toUpperCase());
				String includePath = m.group(getIncludePathMatcherGroup());
				TextResource includedResource = ResourceUtil.getTextResource(includePath, resourceLoader);
				if (includedResource != null) {
					sb.append(resource.getContent().substring(offset, m.start()));
					resourceOffset += m.start() - offset;
					resourceOffset = processResource(resource, includedResource, includeType, sb, resourceOffset, resourceLoader);
					offset = m.end();
				} else {
					throw new XQMissingResourceException(includePath);
				}
			}
			resourceOffset += resource.getContent().length() - offset;
			sb.append(resource.getContent().substring(offset));
			return resourceOffset;
		} catch (IOException ex) {
			throw new XQException("Error processing query source", ex);
		}
	}
	
	private int processQuerySource(String querySource, XQueryResource.Type resourceType, StringBuilder sb, int resourceOffset, ClassLoader resourceLoader) throws XQException {
		try {
			if (resourceType == XQueryResource.Type.MAIN) {
				parseVariables(querySource);
			}
			Matcher m = getIncludePattern().matcher(querySource);
			int offset = 0;
			while (m.find()) {
				XQueryResource.Type includeType = XQueryResource.Type.valueOf(m.group(getIncludeTypeMatcherGroup()).toUpperCase());
				String includePath = m.group(getIncludePathMatcherGroup());
				TextResource textResource = ResourceUtil.getTextResource(includePath, resourceLoader);
				if (textResource != null) {
					sb.append(querySource.substring(offset, m.start()));
					resourceOffset += m.start() - offset;
					resourceOffset = processResource(null, textResource, includeType, sb, resourceOffset, resourceLoader);
					offset = m.end();
				} else {
					throw new ResourceNotFoundException(includePath);
				}
			}
			resourceOffset += querySource.length() - offset;
			sb.append(querySource.substring(offset));
			return resourceOffset;
		} catch (IOException ex) {
			throw new XQException("Error processing query source", ex);
		}
	}

	private void parseVariables(String querySource) {
		Matcher m = getVariablePattern().matcher(querySource);
		while (m.find()) {
			String varName = m.group(getVariableNameMatcherGroup());
			String varType = m.group(getVariableTypeMatcherGroup());
			String varMult = m.group(getVariableMultiplicityMatcherGroup());
			Multiplicity multiplicity = Multiplicity.getMultiplicity(varMult);
			XQVariable var = new XQVariable(varName, varType, multiplicity);
			variableSet.add(var);
			variables.put(varName, var);
		}
	}
}
